AWS CDKで作ったAPIを使ってSalesforceのWebサイトURLからドメインを取り出してみた
クラスメソッド営業統括本部 CRMチーム 進地 です。 Salesforceに保持しているWebサイトURLからドメインを抜き出したいという要望がありましたので、諸々調査してみました!
やりたいこと
- AWS CDKを使ってAPI GatewayにREST APIを作成。このAPIはURLをパラメータで受け取って、そのドメインを抽出して返す。
- Salesforceにて取引先を作成するタイミングで1で作ったAPIをコールして、結果を受け取る。コールする時に「取引先.Webサイト(API参照名: Account.Website)」の値をパラメータとしてAPIに渡す。
- 受け取った結果をSalesforceの取引先に保存する。
このエントリーでは2までを実施してみます(3は結構やっかい。後日別エントリーで補完できるかな?)
やってみた
AWS CDKを使ってAPI GatewayにREST APIを作成する
AWS CDK Intro WorkshopのAPI Gatewayのページを参考にREST APIを作りました。
まず、CDKプロジェクトを新規に作ります。ここではプロジェクト名をdomain-extractor
としました。
$ cdk init domain-extractor $ cd domain-extractor
次に、リソースを定義します。言語にはTypeScriptを選びました。lib/domain-extractor-stack.ts
を次のように書きました。
import * as cdk from 'aws-cdk-lib'; import * as lambda from 'aws-cdk-lib/aws-lambda'; import * as apigw from 'aws-cdk-lib/aws-apigateway'; import { ServicePrincipal } from 'aws-cdk-lib/aws-iam'; export class DomainExtractorStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); const domainExtractor = new lambda.Function(this, 'DomainExtractorHandler', { runtime: lambda.Runtime.NODEJS_16_X, code: new lambda.AssetCode('lambda'), handler: 'domainExtractor.handler' }); domainExtractor.grantInvoke(new ServicePrincipal('apigateway.amazonaws.com')); const api = new apigw.LambdaRestApi(this, 'domainExtractor', { handler: domainExtractor, proxy: false, deployOptions: { loggingLevel: apigw.MethodLoggingLevel.INFO, dataTraceEnabled: true, metricsEnabled: true, } }); const extractor = api.root.addResource('extractor'); extractor.addMethod('POST'); } }
Lambdaファンクションをまず定義し、次にそのLambdaファンクションを呼び出すAPI Gatewayを定義しています。
なお、
domainExtractor.grantInvoke(new ServicePrincipal('apigateway.amazonaws.com'));
の指定がないと、API Gatewayから定義したLambdaが呼び出せずPermissionエラーになります(ここは結構ハマった)。
[apigateway] grant lambda invoke permission when new stages are added #3983
次に、Lambdaファンクションの本体(DomainExtractorHandler
)を作成します。
$ cd lambda
lambdaディレクトリに移動して、domainExtractor.js
を次のように書きました。
const url = require('url'); const psl = require('psl'); exports.handler = async function(event) { console.log("request:", JSON.stringify(event, undefined, 2)); const body = JSON.parse(event.body); const parsed = psl.parse(url.parse(body.url).hostname); const returnValue = { domain: parsed.domain }; return { statusCode: 200, headers: { "Content-Type": "application/json" }, body: JSON.stringify(returnValue, null, 2) }; };
urlモジュールを使ってパラメータで受け取ったURLのホスト名(hostname)を抽出し、それをpslモジュールのparseメソッドに渡すことでドメインを抽出しています。
pslモジュールはlambdaディレクトリ直下でnpm install
する必要があることに注意が必要です。
$ pwd domain-extractor/lambda $ npm install psl
あとは、デプロイして、
$ cdk deploy : DomainExtractorStack.domainExtractorEndpointEC1F9D14 = https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/
作成したAPIの動作確認をしてみます。
$ curl -X POST -d '{"url": "https://dev.classmethod.jp/"}' https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/extractor : { "domain": "classmethod.jp" }
期待通りドメインが返りました。
Salesforceにて取引先を作成するタイミングで1で作ったAPIをコールして、結果を受け取る
手前味噌ですが、私が過去に書いた次のエントリーを参照して作成したAPIをコールします。
Salesforceでの指定ログイン情報の作成は、参照先記事の方法で実施済みとします(名前をAWS_APIGW
として定義したものとします)。
取引先を作成するタイミングで実行したいので、取引先(Account)に対するApex Triggerを作成します。
triggers/AccountTrigger.trigger
を次のように実装してみました。
trigger AccountTrigger on Account (before insert) { new AccountTriggerHandler().execute(); }
処理の実体はAccountTriggerHandler
というApexクラスの方で行います。
public with sharing class AccountTriggerHandler extends TriggerHandler { /** * 作成前処理 * @param newList 作成した取引先 */ public override void beforeInsert(List<Sobject> newList) { // Webサイトの値を取り出し、APIをコールする for (Account acc : (List<Account>)newList) { if (acc.Website != null) { DomainExtractorCalloutAsync.execute('{"url": "' + acc.Website + '"}'); } } } }
TriggerHandler
はTrigger作成用の汎用抽象クラスです。作っておくと便利ですよ。
public abstract class TriggerHandler { public void execute() { switch on Trigger.operationType { when BEFORE_INSERT { this.beforeInsert(Trigger.new); } when BEFORE_UPDATE { this.beforeUpdate(Trigger.oldMap, Trigger.newMap); } when BEFORE_DELETE { this.beforeDelete(Trigger.oldMap); } when AFTER_INSERT { this.afterInsert(Trigger.newMap); } when AFTER_UPDATE { this.afterUpdate(Trigger.oldMap, Trigger.newMap); } when AFTER_DELETE { this.afterDelete(Trigger.oldMap); } when AFTER_UNDELETE { this.afterUndelete(Trigger.newMap); } } } protected virtual void beforeInsert(List<Sobject> newList) {} protected virtual void beforeUpdate(Map<Id, Sobject> oldMap, Map<Id, Sobject> newMap) {} protected virtual void beforeDelete(Map<Id, Sobject> oldMap) {} protected virtual void afterInsert(Map<Id, Sobject> newMap) {} protected virtual void afterUpdate(Map<Id, Sobject> oldMap, Map<Id, Sobject> newMap) {} protected virtual void afterDelete(Map<Id, Sobject> oldMap) {} protected virtual void afterUndelete(Map<Id, Sobject> newMap) {} }
AccountTriggerHandler
の中から、実際のAPIのコールアウト処理はさらにDomainExtractorCalloutAsync
クラスで行っています。
global with sharing class DomainExtractorCalloutAsync implements Database.AllowsCallouts { @future(callout=true) public static void execute(String body) { Http http = new Http(); HttpRequest req = new HttpRequest(); req.setEndpoint('callout:AWS_APIGW/prod/extractor'); req.setMethod('POST'); req.setHeader('Content-Type', 'application/json'); req.setHeader('Accept', 'application/json'); req.setBody(body); HttpResponse res = http.send(req); CalloutResponse cres = (CalloutResponse)JSON.deserializeStrict(res.getBody(), CalloutResponse.class); System.debug(cres.domain); } class CalloutResponse { String domain; } }
Salesforceで取引先を新規作成してみて、開発者コンソールのログにAPIの実行結果(取引先のWebサイトから抽出したドメイン)が記載されていることを確認します。ここでは取引先のWebサイトにhttps://dev.classmethod.jp/を指定してみました。
期待する結果(classmethod.jp
)が取れていますね。